作為一個開發者,遍歷一個字串,一個物件,一個陣列,確認裡面的屬性與值,是再常見不過的場景。
在 ES 6 以前,這些場景和 for 相關的語法總脫不了關係,比如
let str = "string"
let newStr = "";
for(let i = 0; i < str.length; i++) newStr += str[i] + ","
console.log(newStr);//"s,t,r,i,n,g,"
但為了應對更全面的場景,ES 6 推出了一些新的資料結構如:
Object,主要處理鍵值對類型的資料,差異在 Map 的鍵可以為任意型別,而 Object 的鍵只能為字串或 Symbol。同時,Map 的鍵插入操作序會影響其中的儲存順序,Object 則是依自己的一套邏輯來排序鍵。此外,Map 提供了一些方便的屬性,如 size 等可以直接存取鍵數量的屬性。
let demoMap = new Map();
demoMap.set(1,'val1');
demoMap.set({},'val2');
console.log(demoMap.size);//2
Set 就是一個類似 Array,但保證內容不重複的陣列。同時因其底層實作更貼近使用雜湊表(Hash Table)的方式,在新增,插入,刪除,查找的時間複雜度接近 O(1)(陣列是 O(n)),效能更好。arr[0])的方式,同時 Set 也不提供排序功能,需要有特定順序的集合時,還是得使用 array。Set 和 array 做交互轉換的情況。這兩種資料結構都基於 ES 6 推出的迭代器規範來實作的。
接下來,讓我們看看實作迭代器規範是怎麼一回事。
如何遍歷一個可能含有多個值,多個屬性的資料結構,通常是一個資料結構的實作重點之一。
ES 6 的迭代器定義了一個介面,所有的迭代器物件都要符合這些規則和屬性。
迭代這個詞可能對比較沒看過的人有點拗口,迭代沒有上下文的時候指的是「交換替代」(Ref. 教育部國語辭典),用在數學和科學領域,這個詞一般指重複進行某個過程,且每次的操作都基於前一次的結果。
用資料集合的概念就是,每次做一樣的拿取行為,但資料結構自身會記憶訪問位置,每次訪問拿到移動後位置的值,且更新記憶的位置。透過不斷使用相同的拿取方法,可以依此方式不斷訪問取值直至迭代內容的終點。
來看看一個最基本的迭代器會長怎麼樣。(Code Source:MDN)
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let nextIndex = start;
  let iterationCount = 0;
  const rangeIterator = {
    next() {
      let result;
      if (nextIndex < end) {
        result = { value: nextIndex, done: false };
        nextIndex += step;
        iterationCount++;
        return result;
      }
      return { value: iterationCount, done: true };
    },
  };
  return rangeIterator;
}
//Use
const iter = makeRangeIterator(1, 10, 2);
let result = iter.next();
while (!result.done) {
  console.log(result.value); // 1 3 5 7 9
  result = iter.next();
}
console.log("Iterated over sequence of size:", result.value); // [5 numbers returned, that took interval in between: 0 to 10]
這是一個基本的迭代器,內部透過 nextIndex 記住了下一次 next() 方法訪問的對象(配合 step 定義如何更新指向位置)。
建構的時候通過 start 和 end 定義迭代器的資料範圍,當 next() 訪問超過 end 的時候就是到達迭代範圍的盡頭。
(順便提一下,nextIndex 和 iterationCount 即是透過閉包留存的,有注意到嗎?想了解更多關於閉包,請看 Day 12 的內容)
所以上面的例子最後印出來的結果會是 1 3 5 7 9,因為建構時設置 step 為 2,起始為 1,終點為 10。每次 +2,會在 9 停下。
一般來說,在實作迭代器的 next() 方法時,會回傳一個帶有兩個屬性的物件:{done:boolean, value: the value}。
done 會回傳一個布林值,表示目前迭代器是否已到迭代範圍的終點,無法提供下一個值,value 則回傳這次呼叫 next() 時指向的值。
如果 done 為 true,表示迭代已達終點,value 可不回傳或回傳 undefined(無論哪種,訪問時都會回傳 undefined)。
使用迭代器遍歷時,就是使用 next() 方法持續呼叫直到到達迭代範圍的終點。
有個詞叫做可迭代物件(Iterables),指的是該物件具有能被迭代方法 for of 訪問的方式,如 Array 或 Map 都有,但像 Object 就沒有。
要是一個可迭代物件,自身或原型鏈上的物件必須持有 Symbol.iterator 屬性,同時該屬性對應一個方法 [Symbol.iterator]() ,該方法需返回一個迭代器物件。
這段敘述可以看出可迭代物件和迭代器是不同的東西,迭代器更像是一種物件的實作規範,可迭代物件是另一種物件實作規範,其中可迭代物件規範了其一個屬性必須要回傳一個迭代器物件。
JS 中的常見可迭代物件包含 String,Array,Set,Map,Arguments ... 等等,還有下一篇會提到的 Generator 都是可迭代物件。
我們以陣列為例:
let arr = [1,2,3,4,5];
let iterator = arr[Symbol.iterator]();
console.log(iterator.toString());//[object Array Iterator]
console.log(iterator.next());//{done: false,value: 1}
console.log(iterator.next());//{done: false,value: 2}
可以看到例子中的 arr[Symbol.iterator]() 返回的 iterator 物件就是一個迭代器,而 arr 是一個可迭代物件。
相對陣列而言,迭代器是一個更高層的概念,透過迭代器的規範,我們能更好地去訪問實作了迭代器規範的物件內容。
想要特別提一下有一個和「可迭代」聽起來很像,但其實不一樣的觀念「可枚舉」。
枚舉一詞指的是逐一列出枚舉物件可枚舉的所有屬性或方法的過程。
針對物件的屬性或方法,有一個屬性 enumerable 可以被設定,原型鏈上的方法一般預設為 false(大多討論枚舉的情況都是針對屬性,後面我會忽略方法,都簡稱為可枚舉屬性),一般情況下屬性則是預設為 true。可枚舉指的是當進行枚舉行為時會列出來的東西,即 enumerable 為 true 的屬性。
class Human {
	constructor( name ) {
		this.name = name;
	}
	hello() {
		console.log(`${this.name} says Hello`);
	}
}
class Classmate extends Human {
    constructor(name, studentId) {
        super(name); //使用 Human 的建構方法建構
        this.studentId = studentId;
    }
    showId() {
        console.log(`My student Id is ${this.studentId}`);
    }
}
let friend2 = new Classmate('Ryu','10000');
console.log(friend2.propertyIsEnumerable('name')); // true
console.log(friend2.propertyIsEnumerable('hello')); // false
console.log(friend2.propertyIsEnumerable('showId')); // false
for in 就是一個基於可枚舉實現的方法,當一個物件是可枚舉的,就可以使用 for in 來遍歷他的可枚舉屬性。
可枚舉的物件不一定可迭代,可迭代的物件也不一定可枚舉。
Object
let obj = {k1:'val1', k2:'val2'};
for (let key in obj) {
console.log(key);//k1, k2
}
for (let value of obj) {
console.log(value); //TypeError: obj is not iterable
}
Set
let set = new Set([1,2,3]);
for (let value of set) {
    console.log(value); //1 2 3
}
for (let key in set) {//不會報錯,但也不會執行,不會印出任何值
    console.log(key); 
}
Array
let arr = ['a','b','c'];
for (let value of arr) {
    console.log(value); //"a" "b" "c"
}
for (let key in arr) {
    console.log(key); //0 1 2
}
通常討論可枚舉跟可迭代就是以 for in 和 for of 來舉例。
可以看到 for in 對應可枚舉,且以回傳索引為主,for of 則是回傳值、對應可迭代。
針對陣列的訪問,會更建議使用 for of 而非 for in,因為 for in 會列出所有可枚舉對象,而 for of 則只會列出可迭代對象,更貼近我們一般遍歷陣列內容時希望的場景。
const arr = [10, 20, 30];
arr.foo = "bar";//非陣列索引的一環,附在陣列上的屬性
for (let key in arr) {
  console.log(key); // "0" "1" "2" "foo"
}
for (let val of arr){
	console.log(val);//10 20 30
}
一旦一個物件實作了迭代器規範,則表示該物件能夠使用下列函式 / 運算子。
for of
上面提到的,也是可迭代物件的定義。
需要能夠執行 for of 來遍歷才是一個可迭代物件。
展開運算子(...)
let a = [1, 2, 3];
let b = [0,...a]
console.log(b);//[0, 1, 2, 3]
解構運算子(let [a,b] = [1,2])
要順便提到的是除了可迭代物件能使用解構運算子之外,一般物件也能使用,但兩者的原理是不同的。
可迭代物件是基於他本身的可迭代性,依其順序進行迭代提值進行解構;物件則是針對鍵值對進行解構,解構出來的必須要對應鍵的名稱,與順序無關。
//可迭代的解構
let set = new Set([1, 2, 3, 4]);
let [first, second] = [...set];
console.log(first);  // 1
console.log(second); // 2
//一般物件的鍵值對解構
let obj = {foo:'foo', bar:'bar'};
let {foo, bar} = obj;
console.log(foo);//"foo"
console.log(bar);//"bar"
Array.form()
用於將 類陣列物件(Array-Like) 或 可迭代物件(Iterables) 轉為陣列的靜態方法。
類陣列物件又與可枚舉,可迭代兩詞有出入,類陣列的先決條件是:
因為只是類陣列,類陣列也不保證能夠使用陣列的方法。總是是一個額外的類別定義。
總之類陣列物件或可迭代物件都可以使用這個方法來轉為陣列,轉換完便能夠使用陣列的那些方法,也具有陣列的那些屬性。
let set = new Set([3, 1, 2, 3, 3]);
console.log(set[0]);//undefined,Set 不是一個枚舉物件
let arr = Array.form(set);
console.log(arr[0]);//3,陣列是可枚舉物件,可透過索引訪問元素
console.log(arr);//[3, 1, 2]
有了迭代器的觀念,我們可以接著討論生成器(Generator)和相關的語法了。